C语言基础 | 您所在的位置:网站首页 › c语言 call › C语言基础 |
文章目录
一、初识指针二、指针和指针类型指针类型的意义1)指针的解引用①问题抛出②探讨③总结
2)指针+整数3)总结4)举例
三、野指针(1)概念1) 指针未初始化2)指针越界访问3)指针指向的空间释放
(2)规避野指针1)指针初始化2)小心指针越界3)指针指向的空间释放即使置NULL4)指针使用之前检查有效性
四、指针运算(1)指针加减整数1)指针+整数2)指针-整数3)案例
(2)指针减指针1)案例一2)错误案例3)案例二
(3)指针的关系运算
五、指针和数组(1)回顾数组(2)指针和数组
六、二级指针(1)介绍(2)理解
七、指针数组
一、初识指针
指针是什么? 在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,该地址指向该变量单元。因此,将地址形象化的称为:指针。意思是,通过它能找到以它为地址的内存单元。 ❓ 问: 1、 编号如何产生? (1) 电脑上有地址线,地址线一旦通电,就能产生电信号,把电信号转为数字信号,数字信号产生的二进制序列(1或0),把数字信号作为二进制的编号,可作为内存单元的一个编号,即内存单元的地址。十六进制显得方便。 (2) 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或0)。 (3) 那么32根地址线产生的地址就会是: 这里就有二的三十二次方个地址。 每个地址标识一个字节,那我们就可以给(2^32byte==2^32/1024kb==2^32/1024/1024mb==2^32/1024/1024/1024gb==4gb)4G的空闲进行编址。 同样的方法,64位机器,如果给64根地址线,能编址多大空间,自己计算。 这里我们就明白: 1) 在32位机器上,地址是32个0或1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。 2) 在64位机器上,如果有64个地址线,那一个指针变量的大小是8字节,才能存放一个地址。 2、 一个内存单元是多大? 一个字节比较合适。 P里面存的是地址,地址也叫指针。p变量是用来存放地址的,所以叫它指针变量,类型是int*。 存放地址的,就称为指针变量,而这个指针变量存放的又是地址。 指针就是地址,地址就是指针。 🙅 指针其实就是个变量,变量里面存放的是内存单元的地址,也就可以说:指针就是地址。(存放在指针中的值都被当成地址处理) 🍰 总结: 指针是用来存放地址的,地址是唯一标示一块地址空间的。 指针的大小在32位平台是4个字节,在64位平台是8个字节。 二、指针和指针类型 指针类型的意义我们来看一下,各种指针类型的大小: 当前默认是32位平台,所以结果都是4个字节。 1)指针的解引用 ①问题抛出❓ 问:它们指针大小都是4个字节,那为什么要区分? 那指针类型究竟有什么样的意义? 先定义一个整型变量a。 int a = 0x11223344;将十六进制数0x11223344存入a中。 可以放进去吗? 可以的哦,两个十六进制位占一个字节(1个十六进制是4个二进制位,2个十六进制位就是8个二进制位,即1个字节)。 这样的话,11占一个字节,22占一个字节,33占一个字节,44占一个字节。总共4个字节。 a是整型变量,4个字节的空间。所以是可以存放的。 再将a的地址取出来(&a)。 放在变量pa里面,pa的是指针类型的变量,即int*类型。 如下: int* pa=&a;既然在探讨指针类型,那么我们将a的地址放入char*类型的变量pc里面行吗? 如下: char* pc=&a;pa和pc都能存放好a的地址吗? 来输出看一下: 由上图可见,两次输出地址一样。 可见,无论是什么类型的,都能存好a的地址。 为啥都能存放?因为pa和pc都是指针变量,都是4字节(32平台)/8字节(64平台)大小。 类型不匹配,只会报警告,类型不兼容。但还是可以存放进去的。 既然无论什么类型都能存放,那指针类型还有什么意义了呢? 一起往下继续探讨吧! ②探讨接下来,我们分别看一下不同类型存储a地址的内存。 当我们用Int类型来存储时 现在我们将a变量的地址存入一个整型指针pa中。 int a = 0x11223344; int* pa = &a; *pa = 0;按F10进行调试,当左边的小黄色箭头指在第二行的时候,打开内存(调试–>窗口–>内存): 输入&a: 左边是地址,右边是内存(如下): 可以看见,内存中存放的是:44 33 22 11。 至于为什么是倒着存放的,这里不做探讨。 再次按F10进行调试,左侧黄色箭头指向pa(第三行)。这一行的代码意思是将pa指针指向的a的值变成0。 再次按F10进行调试,左侧黄色箭头指向最后一行(第四行)。 可以发现,右侧的内存中显示,a内存里面的值变成了0。 如下: 画个图演示一下a内存中值的变化: 当我们用char类型来存储时 int a = 0x11223344; char* pc = &a; //pc为字符指针 *pc = 0;现在我们将a变量的地址存入一个字符指针pc中。 按F10进行调试,当左边黄色箭头指向第二行时,在右侧输入&a: 可以看见内存中存放的的确是a变量的值:44332211(顺序不用在意) 按F10继续调试,可以看到程序走完,右侧的内存中的数值变化: (当我们将0赋值给pc指针指向的a变量,a内存中只改变了部分值) 内存中4个字节,这里只改变了1个字节。 当用来存储的变量的类型发生变化,我们解引用操作的时候,结果不一样。 以上结果我们看到,用Int类型来存储时,当进行数据改变,原来的都改变了;而用char类型来存储时,只改变了前两个数(1个字节)。 🍰 指针在存储数据的时候,类型无差,都能够存放。但当我们对它进行解引用操作的时候,整型指针,可以改4个字节;而字符指针,确实可以从指针访问相应空间,但*pc去访问空间的时候,只能改1个字节。 ③总结指针类型决定了指针进行解引用操作的时候,能够访问空间的大小。 不同类型的指针能够访问的空间大小不同: int* p; p能够访问4个字节 char* p; p能够访问1个字节 double* p; p能够访问8个字节那么学这个类型有什么意义? 指针类型既然决定了指针变量解引用能够一次访问几个字节,那当我们给指针赋值的时候,应该赋一个合理的指针。 比如我们希望从这个位置向后访问一个字节,那我们应该把它交给一个char指针;我们希望一次访问两个字节,那我们应该把它交给一个short类型的指针。 2)指针+整数上面我们讨论了第一个意义,下面来讨论第二个意义。 如下代码: int a=0x11223344; int* pa=&a; char* pc=&a; printf("%p\n",pa); printf("%p\n",pa+1); printf("%p\n",pc); printf("%p\n",pc+1);看一下最终结果: 我们可以看到,当指针类型是int类型时,pa+1向后访问了4个字节; 而指针类型是char类型时,pc+1向后访问了1个字节。 即指针类型决定了指针加一向后跳几个字节,指针走一步走多远(步长)。 int* p; p+1 --> 4 char* p; p+1 --> 1 double* p; p+1 --> 8❓ 指针向前向后一次走多大? 向后走的是指针所指向对象的类型的大小,整型指针向后加的是4,因为向后加一是跳过一个整型(4个字节);字符指针向后加一加的是1个字节,因为向后跳的是1个字符。 3)总结🍰 总结 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。比如:char的指针解引用就只能访问一个字节,而Int的指针的解引用就能访问四个字节。 指针的类型决定了指针向前或者向后走一步走多大(距离),单位是字节。 4)举例那我们知道指针类型的意义有什么用? 我们来举个例子: int arr[10] = { 0 };将arr数组的地址拿出来,赋值给指针变量p: int* p=arr; //arr为数组名,即首元素地址🚗 需求:把arr数组里面的元素全部改成1。 用int类型的指针 改变数组第一个元素:*(p+0)=1 p指针指向数组第一个元素地址,解引用之后就是第一个元素,再将1赋值给它即可。 改变数组第二个元素:*(p+1)=1 p指针向后移动一位,指向数组第二个元素地址,解引用之后就是第二个元素,再将1赋值给它。 后面的规律就找到了。 改变数组里面元素:*(p+i)=1。(i=0,1,…) 这样分析,就可以写出如下代码: for (i=0;i 0 }; int* p=arr;//arr为数组名,即首元素地址 int i = 0; for (i=0;i printf("%d ",arr[i]); }我们来看一下内存:(每4个字节改变一下) 用char类型的指针 刚才我们使用了int类型的指针,那么使用char类型的话,会怎么样呢? int arr[10] = { 0 }; char* p=arr;//arr为数组名,即首元素地址 int i = 0; for (i=0;i int a; //局部变量不初始化,默认是随机值 int* p; //局部变量指针未初始化,默认为随机值 *p=20; return 0; }随机生成一个地址是很可怕的。 通过p随机找到一块内存空间,改变它的值,这是非法操作。 编译器也会报错: 2)指针越界访问当指针指向的范围超出数组arr的范围时,p就是野指针。 #include int main(){ int arr[10]={0}; int* p=arr; int i=0; for(i=0;i int arr[10]={10}; return arr; } int main(){ int* p=test(); printf("%d\n",*p); }只要是返回临时变量的地址,都是有问题的! 除了这个临时变量没有被销毁。被static修饰就不会被销毁。 (2)规避野指针 1)指针初始化一定要记得初始化!!! int main(){ int a=10; int* pa=&a; //初始化 }但我们也会遇到不知道怎么初始化的时候。 这时候就可以给它赋值NULL。(空指针) 如下: int* p=NULL;这个NULL是什么呢? 我们点击NULL,速览定义: 可以看到,NULL就是0: NULL用来初始化指针的,给指针赋值。 (void*)0:把0强制类型转换成了void*这种类型,本质上还是0。 就相当于之前我们创建变量的时候,不知道赋什么值,就给变量赋值0: int b=0; 2)小心指针越界这个没有什么好解释的,上面已经说的很明白了。 3)指针指向的空间释放即使置NULL指针指向的空间,如果我们还给别人了,我们就可以把指针置为空。 本来指针指向了这块空间,现在我不想用这块空间了,这块空间已经还给别人了。 如果还想让这个指针合法的存在,那就先把它设为空指针。 举个例子: 当你不想让一个指针指向其他地方的时候,或者它指向的空间已经还给操作系统的时候,这时候可以把一个指针置成空指针。 避免它未来可能成为野指针。 4)指针使用之前检查有效性当我们对指针进行初始化,这个指针就是有效的。 当我们不用这个指针的时候,我们把它置成空指针(置成空指针的时候,就不能访问它指向的空间了)。 即:遇到指针初始化,用完之后赋值为NULL空指针。 就像这样: ①用的时候初始化 int a=10; int* pa=&a; *pa=20;②不用的时候置为空指针 pa=NULL;所以, 在后边我们要继续使用这个指针的时候,就需要判断一下:这个指针,如果等于空指针就不用;不是空指针就使用。 即: if(pa==NULL){ //如果pa是空指针,就不使用 } if(pa!=NULL){ //如果pa不是空指针,就可以使用它 }当pa已经被赋值为空指针,这时候我们强制访问它,并给它赋值为10。 调试看一下: 代码走着走着就挂了,程序崩溃了。 ⛔️ 当指针为NULL的时候,不能访问它! 所以后边使用指针的时候,必须要判断一下。 如果指针不是空指针(说明里面放的是有意义的地址),就可以使用;是空指针就不能使用。 如下判断即可: 看个小案例,指针被赋值为了空指针,重新给它指向空间(初始化),就可以继续使用了。 如下: 四、指针运算 (1)指针加减整数 1)指针+整数看一个小案例: int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //不用下标来访问,用指针来访问 int* p = arr; //数组名就是首元素地址,将arr交给p指针 int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); //数组元素个数 for (i = 0; i int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; //不用下标来访问,用指针来访问 int* p = &arr[9]; //将第十个元素的地址交给p指针 int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); //数组元素个数 for (i = 0; i float values[N_VALUES]; //定义一个数组,数组里面5个元素 float* vp; //定义一个指针 for(vp=&values[0];vp int arr[10]={1,2,3,4,5,6,7,8,9,10}; printf("%d\n",&arr[9]-&arr[0]); //让第10个元素的地址减去第1个元素的地址 }输出看一下: 🍰 指针减去指针:得到的是中间的元素个数。 中间元素有:1,2,3,4,5,6,7,8,9(一共9个) 如下图: 如果是小地址减去大地址,得到的就是负数(反过来了)。 如下图:(得到了-9) 所以, 想得到元素个数,一定是大地址减去小地址,而小地址减去大地址的绝对值是最终结果。 2)错误案例①不同类型 再来看一个错误: int main(){ int arr[10]={1,2,3,4,5,6,7,8,9,10};//整型数组 char ch[5]={0};//字符数组 printf("%d\n",&arr[9]-&ch[0]); }以上这种情况,是按照整型讨论还是字符?乱套了!!!这种写法最终结果是不可预知的。 当一个指针减去一个指针的时候,那这两个指针一定是指向同一块空间的。 这是错误写法!两个不同类型的指针相减,是没有任何意义的。 ②指针相加 ⛔️ 注意:两个指针相加也没有意义! 3)案例二再举个例子:求字符串长度 现在我们拥有一个数组arr,将数组首元素地址传给了my_strlen函数。 int my_strlen(char* str) { } int main() { //strlen-求字符串长度 //讲“递归”的时候,我们模拟实现了strlen,1、递归的方式 2、计数器的方式 char arr[] = "bit"; int len=my_strlen(arr);//把数组的首元素地址放进去了 printf("%d\n", len); return 0; }🌵 分析: 如果现在有一个指针(start)指向数组第一个元素,还有一个指针(end)指向最后一个元素。 那么,就可以用end指针减去start指针,就可以得到字符串长度了。 那么这两个指针如何表示? start指针很简单,我们传上去的就是数组首元素地址,直接赋值给start指针即可。 char* start=str;end指针可以利用循环,找到最后的元素\0,停止循环,即可找到最后元素的地址。 char* end=str; while(*end !='\0'){ end++; }最后返回元素个数(end-start): return end-start;看一下编译器输出结果: 整体代码如下: int my_strlen(char* str) { char start = str; char end = str; while (end != '\0') { end++; } return end - start;//字符个数 } int main() { //strlen-求字符串长度 //讲“递归”的时候,我们模拟实现了strlen,1、递归的方式 2、计数器的方式 char arr[] = "bit"; int len=my_strlen(arr);//把数组的首元素地址放进去了 printf("%d\n", len); return 0; } (3)指针的关系运算指针的关系运算即指针比较大小。 #define N_VALUES 5 int main() { float values[N_VALUES]; //创建一个数组,里面5个元素 float* vp; for (vp = &values[N_VALUES]; vp > &values[0];) { *--vp = 0; } return 0; }画个图帮助理解: 把上面代码改一下,第二种方法:(注意看for循环) #define N_VALUES 5 int main() { float values[N_VALUES]; float* vp; for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--) { *vp = 0; } return 0; }画个图演示一下: 方法二,实际在绝大部分编译器上是可以顺利完成任务的,然而我们应该避免这样写,因为标准并不保证它可行。 📖标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存未知的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。 所以,尽量使用第一种方法,第二种方法避免使用! 五、指针和数组 (1)回顾数组先回顾一下: 数组名是什么? 数组名是首元素地址(两个特例)。 数组基础知识传送门:C语言基础–数组_雨翼轻尘的博客-CSDN博客。 既然数组名是首元素地址,那么我们打印arr和&arr[0]结果应该是一样的。 如下图: arr与**arr[0]**地址相同。所以都是首元素地址,这是无疑的。 绝大多数情况下,数组名都是首元素地址。 有两个例外: ①&arr 整个数组的地址。 ②sizeof(arr) 整个数组的大小,单位为字节。 arr与&arr[0]与&arr的区别莫过于这张图: 🍰 总结 arr和&arr[0]得到的是首元素地址,也仅仅只有首元素地址。 而&arr得到的地址,后面包括一整个数组。 可能上面说的比较含糊,如果将它们分别加一,观察分别输出的地址: 有的小伙伴可能不太会地址的计算,之前博客有写过,这里再说一下吧。 内存中地址是十六进制存储的,1~9,a~f(10~15)。 地址计算:(案例) 00EFF8E0 — > 00EFF908 将后面三项拿出来:8E0 — > 908 怎么计算相差多少呢?如下图: 现在我们来算一下当前的结果: 计算过程如下图: (2)指针和数组既然数组名表示首元素地址,那么就可以直接将数组名存入指针里面,如下: int arr[10]={1,2,3,4,5,6,7,8,9,0}; int* p=arr; //p里面存放的是数组首元素地址既然可以把数组名当成地址存放到一个指针中,我们就可以使用指针来访问数组。 即,数组可以通过指针进行访问。 我们不妨通过两种方法,分别输出每一个元素的地址。 第一种,可以这样输出数组元素的地址:&arr[i];第二种:p+i。 具体代码如下: int main() { int i = 0; int arr[] = { 1,2,3,4,5,6,7,8,9,0 }; int* p = arr; //p里面存放的是数组首元素地址 int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i int i = 0; int arr[10] = { 0 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i printf("%d ", arr[i]); //用数组形式,输出数组里面元素 } printf("\n"); for (i = 0; i int a=10; int* pa=&a; //pa就是一级指针变量,int*就是一级指针类型 }a是一个变量,将a的地址取出来,放进pa里面,pa的类型是int*。pa是一级指针变量。 再来想一下,pa是指针变量,变量创建要在内存中开辟空间。 如果现在这样写:&pa,就拿到了pa空间的地址。这块地址也想存起来,怎么办呢? 比如将pa的地址,存放进ppa变量里面,这时候ppa的类型就应该这样写:int**。 如下: int** ppa=&pa;ppa就是二级指针(存放一级指针的地址)。 同样,如果想要取出ppa的地址,存放进pppa变量里面。pppa变量的类型应该是int***。 如下: int*** pppa=&ppa;pppa就是三级指针(存放二级指针的地址)。 这里,我们只需要了解二级指针。其他的不用管。 (2)理解int* pa=&a:*表示pa是指针类型的,int表示它指向的对象是int类型。 int* * ppa=&pa;:第二个*表示ppa是指针类型的,int*表示它指向的对象是int**类型。 画个图帮助理解: 二级指针有什么用呢? 比如现在我们想通过ppa拿出a的值,先解引用*ppa,找到pa,然后再解引用**ppa,找到a,输出即可。 如下: printf("%d\n",**ppa);看一下结果输出: 还可以通过ppa改变a的值。 **ppa=20;输出看一下: 所有代码: #define _CRT_SECURE_NO_WARNINGS #include int main() { int a = 10; int* pa = &a;//pa是一级指针变量,int*是一级指针类型 int* * ppa=&pa;//ppa就是二级指针变量 **ppa = 20; printf("%d\n", **ppa); printf("%d\n", a); //int** * pppa = &ppa;//pppa就是三级指针变量 return 0; } 七、指针数组❓ 问:指针数组是指针还是数组? 是数组。是存放指针的数组。 看一个小例子,: int main(){ int a=10; int b=20; int c=30; //分别将a,b,c的地址存入指针变量pa,pb,pc中 int* pa=&a; int* pb=&b; int* pc=&c; }如果我想把a,b,c的地址存起来,就需要三个指针变量pa,pb,pc。 那能不能写一个数组,把他们三个地址都存放起来?数组里面放的都是整型变量的地址。 如何写一个指针数组? 之前我们写整型数组:int arr[3],那么写指针数组就可以这样写:int* arr[3]。 然后初始化: int* arr[3]={&a,&b,&c};拿到指针数组里面的指针也很简单: *(arr[i]); //i=0,1,2输出看一下: 所有代码: int main(){ int a=10; int b=20; int c=30; //分别将a,b,c的地址存入指针变量pa,pb,pc中 int* pa=&a; int* pb=&b; int* pc=&c; int* arr[3]={&a,&b,&c}; int i=0; for(i=0;i |
CopyRight 2018-2019 实验室设备网 版权所有 |